#!/usr/bin/python

import sys
import copy
import ctypes

LIBPATH = "/home/isteve/svn/fel/bakulepoc/trunk/libfdb.so"
HELPERSPATH = "/home/isteve/svn/fel/bakulepoc/trunk/libfdb-helpers.so"
_lib = None
_helpers = None
def get_lib():
	global _lib
	if _lib is None:
		_lib = ctypes.CDLL(LIBPATH, ctypes.RTLD_GLOBAL)

		# Transaction control
		_lib.transaction_begin.restype = ctypes.c_void_p
		_lib.transaction_commit.restype = ctypes.c_int
		_lib.transaction_rollback.restype = ctypes.c_int

		# Conditions
		_lib.condition_predicate_new.restype = ctypes.c_void_p
		_lib.condition_callback_new.restype = ctypes.c_void_p
		_lib.condition_negate.restype = ctypes.c_void_p

		# Query control
		_lib.query_new.restype = ctypes.c_void_p
		_lib.query_prepare.restype = ctypes.c_int
		_lib.query_execute.restype = ctypes.c_int
		_lib.query_step.restype = ctypes.c_int
		_lib.query_finalize.restype = ctypes.c_int
		_lib.query_data_set.restype = ctypes.c_int

		#_lib.query_datatype_get.argtypes = [ ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p) ]
		_lib.query_datatype_get.restype = ctypes.c_int

		# Database control
		_lib.database_open.restype = ctypes.c_void_p

		# Result verification
		_lib.result_eod.restype = ctypes.c_int
		_lib.result_error.restype = ctypes.c_int
		_lib.result_text.restype = ctypes.c_char_p		
	return _lib

def get_helpers():
	global _helpers
	if _helpers is None:
		_helpers = ctypes.CDLL(HELPERSPATH, ctypes.RTLD_GLOBAL)
	return _helpers

# Transaction isolation
DIRTY_READ = 0
READ_COMMITED = 1
REPEATABLE_READ = 2
SERIALIZED = 3

# Query types
QUERY_SELECT = 0
QUERY_INSERT = 1
QUERY_DELETE = 2
QUERY_UPDATE = 3

# Function type
_QUERY_CALLBACK_F = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
def _query_callback_wrapper(func):
	def _f(query_p, internal_ctx_p):
		# We actually don't use the query pointer, we pass our own data.
		internal_ctx = ctypes.py_object.from_address(internal_ctx_p)
		query_obj, ctx = internal_ctx.value
		return func(query_obj, Database.Row(query_obj), ctx)
	return _f


class DatabaseException(Exception):
	pass

class DatabaseResultException(DatabaseException):
	def __init__(self, rvcode):
		Exception.__init__(self)
		self.rvcode = rvcode
		self.rvtext = get_lib().result_text(rvcode)
	
	def __str__(self):
		return '<%s: %d="%s">' % (
			self.__class__.__name__,
			self.rvcode,
			self.rvtext,
		)
	
class DatabaseTransactionException(DatabaseException):
	pass

class Datatype():
	def __init__(self, value, dt_args=None):
		self.value = value
		self.dt_args = dt_args
	
	def to_cell(self, dt_args=None):
		raise NotImplementedError

	@classmethod
	def from_cell(cls, data, size, dt_args=None):
		raise NotImplementedError

class DatatypeInt64(Datatype):
	name = 'int64'

	def to_cell(self, dt_args=None):
		if dt_args is None:
			dt_args = self.dt_args

		val = ctypes.c_int64(self.value)
		return (ctypes.byref(val), 8)

	@classmethod
 	def from_cell(cls, data, size, dt_args=None):
		if dt_args is None:
			dt_args = self.dt_args

		ptr_t = ctypes.POINTER(ctypes.c_int64)
		return cls(ctypes.cast(data, ptr_t).contents.value)

class DatatypeString(Datatype):
	name = 'string'

	def to_cell(self, dt_args=None):
		if dt_args is None:
			dt_args = self.dt_args

		return (str(self.value + '\0'), len(str(self.value)) + 1)
		
	@classmethod
 	def from_cell(cls, data, size, dt_args=None):
		if dt_args is None:
			dt_args = self.dt_args

		return cls(ctypes.string_at(data, size))

class DatatypeArray(Datatype):
	name = 'array'

	def to_cell(self, dt_args=None):
		if dt_args is None:
			dt_args = self.dt_args

		datatype = dt_args[0]

		rawdata = []
		rawsizes = []
		for item in self.value:
			data, size = datatypes[datatype].to_cell(item)
			rawdata.append(data)
			rawsizes.append(sizes)

		
		get_helpers().helper_create_array_list(rawdata, rawsizes, len(self.value), TODO, TODO)

	@classmethod
 	def from_cell(cls, data, size, dt_args=None):
		if dt_args is None:
			dt_args = self.dt_args

		c_size_t_ptr = ctypes.POINTER(ctypes.c_size_t)
		datatype = dt_args[0]

		offset = 0
		items = ctypes.cast(data, ctypes.POINTER(ctypes.c_uint32)).contents.value
		offset += 4  #uint32

		output = []
		for item in xrange(items):
			itemsize_str = data[offset:(offset + ctypes.sizeof(ctypes.c_size_t))]
			offset += ctypes.sizeof(ctypes.c_size_t)

			itemsize = ctypes.cast(ctypes.c_char_p(itemsize_str), c_size_t_ptr).contents.value
			itemdata = data[offset:offset + itemsize]
			offset += itemsize

			output.append(datatypes[datatype].from_cell(itemdata, itemsize, dt_args[1:]))
		return cls(output)

datatypes = {
	'int64': DatatypeInt64,
	'string': DatatypeString,
	'array': DatatypeArray,
}

class Database():
	class Condition():
		def __invert__(self):
			obj = copy.copy(self)
			obj.positive = not obj.positive
			return obj

		def __neg__(self):
			obj = copy.copy(self)
			obj.positive = not obj.positive
			return obj

	class ConditionPredicate(Condition):
		def __init__(self, column_id, predname, *args):
			self.column_id = column_id
			self.predname = predname
			self.args = args
			self.positive = True

		def bind(self, query):
			global datatypes
			callargs = []
			for item in self.args:
				rawdata, length = item.to_cell(None)
				callargs.append(length)
				callargs.append(rawdata)

			condition = get_lib().condition_predicate_new(self.column_id, self.predname, len(callargs) / 2, *callargs)
			if not self.positive:
				condition = get_lib().condition_negate(condition)
			return condition
			
	
	class ConditionCallback(Condition):
		def __init__(self, callback, ctx):
			self.callback = callback
			self.ctx = ctx

		def bind(self, query):
			pass

	class Datatype():
		@property
		def _as_parameter_(self):
			return self._datatype

		def arguments(self):
			pass

	class Table():
		def __init__(self):
			self.columns = {}
	
	class Column():
		def __init__(self, column_id):
			pass

	class Row():
		def __init__(self, query):
			self._query = query

		def __getitem__(self, column_id):
			dataptr_t = ctypes.POINTER(ctypes.c_char)
			data = dataptr_t()
			size = ctypes.c_size_t()

			get_lib().query_data_get(self._query, column_id, ctypes.byref(data), ctypes.byref(size))

			datatype, dt_args = self._query.datatype(column_id)

			global datatypes
			return datatypes[datatype].from_cell(data, size.value, dt_args)
			

	class Transaction():
		def __init__(self, database, isolation):
			self.database = database
			self.isolation = isolation

		@classmethod
		def begin(cls, database, isolation):
			inst = cls(database, isolation)

			inst._transaction = get_lib().transaction_begin(inst.database, inst.isolation)
			if inst._transaction is None:
				raise DatabaseTransactionException
			return inst

		@property
		def _as_parameter_(self):
			return self._transaction

		def commit(self):
			if get_lib().transaction_commit(self) != 0:
				raise DatabaseTransactionException

		def rollback(self):
			if get_lib().transaction_rollback(self) != 0:
				raise DatabaseTransactionException

	class Query():
		def __init__(self, query_type, transaction, tablename):
			self.query_type = query_type
			self.transaction = transaction
			self.tablename = tablename

			self._columns = {}
			self._query = get_lib().query_new(transaction)
			self._executed = False
			if self._query is None:
				raise DatabaseTransactionException

			rv = get_lib().query_prepare(self, query_type, tablename)
			if get_lib().result_error(rv) != 0:
				raise DatabaseResultException(rv)

		@property
		def _as_parameter_(self):
			return self._query

		def datatype(self, column_id):
			if column_id not in self._columns:
				datatypeptr_t = ctypes.c_char_p
				datatype = datatypeptr_t()
				datatypeargsptr_t = ctypes.POINTER(ctypes.c_char_p)
				datatypeargs = datatypeargsptr_t()
				datatypeargc = ctypes.c_int()

				get_lib().query_datatype_get(self._query, column_id, ctypes.byref(datatype))
				get_lib().query_datatype_args_get(self._query, column_id, ctypes.byref(datatypeargs), ctypes.byref(datatypeargc))
				datatype = ctypes.string_at(datatype)
	
				
				dt_args = []
				for item in xrange(datatypeargc.value):
					dt_args.append(datatypeargs[item])
	
				self._columns[column_id] = (datatype, dt_args)
			return self._columns[column_id]

		def execute(self):
			rv = get_lib().query_execute(self)
			if get_lib().result_error(rv) != 0:
				raise DatabaseResultException(rv)
			self._executed = True

		def step(self):
			rv = get_lib().query_step(self)
			if get_lib().result_error(rv) != 0:
				raise DatabaseResultException(rv)

		def finalize(self):
			rv = get_lib().query_finalize(self)
			if get_lib().result_error(rv) != 0:
				raise DatabaseResultException(rv)
			get_lib().query_destroy(self)

			self._query = None

		def set_conditions(self, conditions):
			for oritem in conditions:
				andlist = [ anditem.bind(self) for anditem in oritem ]
				andlist.append(None)
				get_lib().query_append_conditions(self, *andlist)

		def set_data(self, *data):
			global datatypes

			libargs = []
			for column_id, item in enumerate(data):
				if item is not None:
					datatype, dt_args = self.datatype(column_id)
					rawdata, length = datatypes[datatype].to_cell(item, dt_args)
					libargs.append(rawdata)
					libargs.append(length)
				else:
					libargs.append(None)
					libargs.append(0)
			get_lib().query_data_set(self, *libargs)

		def __iter__(self):
			if not self._executed:
				self.execute()
			return self

		def next(self):
			rv = get_lib().query_step(self)
			if get_lib().result_error(rv) == 0:
				if get_lib().result_eod(rv) == 0:
					return Database.Row(self)
				else:
					raise StopIteration
			else:
				raise DatabaseResultException(rv)
			
	
	class QuerySelect(Query):
		def __init__(self, transaction, tablename):
			Database.Query.__init__(self, QUERY_SELECT, transaction, tablename)

	class QueryDelete(Query):
		def __init__(self, transaction, tablename):
			Database.Query.__init__(self, QUERY_DELETE, transaction, tablename)
		
		def step(self):
			raise NotImplementedError  # No data to iterate in DELETE

	class QueryInsert(Query):
		def __init__(self, transaction, tablename):
			Database.Query.__init__(self, QUERY_INSERT, transaction, tablename)
		
		def set_conditions(self):
			raise NotImplementedError  # No conditions affect an INSERT
	
	class QueryUpdate(Query):
		def __init__(self, transaction, tablename):
			Database.Query.__init__(self, QUERY_UPDATE, transaction, tablename)
		
		def step(self):
			raise NotImplementedError  # No data to iterate in DELETE

		def set_callback(self, callback, ctx):
			self.update_callback_ctx = ctypes.py_object( (self, ctx) )
			self.update_callback = _QUERY_CALLBACK_F(_query_callback_wrapper(callback))
			return get_lib().query_update_callback(self, self.update_callback, ctypes.byref(self.update_callback_ctx))
	
	def __init__(self):
		pass

	@classmethod
	def opendb(cls, path):
		inst = cls()

		inst._database = get_lib().database_open(path)
		if inst._database is None:
			raise DatabaseException
		return inst

	@property
	def _as_parameter_(self):
		return self._database

	def closedb(self):
		get_lib().database_close(self)


if __name__ == '__main__':
	# Take the function, and wrap it around.
	def increase_pkey(query, row, ctx):
		x = row[0].value + 1
		qry.set_data(DatatypeInt64(x), None)
		return 0

	db = Database.opendb(sys.argv[1])
	tx = Database.Transaction.begin(db, READ_COMMITED)
	
	print '--seq--'
	qry = Database.QuerySelect(tx, "sequence")
	qry.execute()
	for row in qry:
		print '%s, %s' % (row[0].value, row[1].value)
	qry.finalize()
	print '--seq--'
	print

	print '--orig--'
	qry = Database.QuerySelect(tx, "data")
	qry.execute()
	for row in qry:
		print '%s, %s' % (row[0].value, row[1].value),
	qry.finalize()
	print '--orig--'
	print

	arg = sys.argv[2]
	if "i" in arg:
		print 'INSERT'
		qry = Database.QueryInsert(tx, "table1")
		qry.execute()
		qry.set_data(DatatypeInt64(1000), DatatypeString(sys.argv[2]))
		qry.step()
		qry.finalize()

	if "u" in arg:
		#qry.set_conditions([ [ Database.ConditionPredicate(0, "eq", DatatypeInt64(1000)) ] ] )
		#qry.set_data(None, DatatypeString(sys.argv[2]))
		print 'UPDATE'
		qry = Database.QueryUpdate(tx, "table1")
		qry.set_callback(increase_pkey, None)
		qry.execute()
		qry.finalize()
		print 'UPDATE'
		qry = Database.QueryUpdate(tx, "table1")
		qry.set_callback(increase_pkey, None)
		qry.execute()
		qry.finalize()
		print 'Press any key.'
		sys.stdin.read(1)
		print 'Any key pressed.'

	if "d" in arg:
		print 'DELETE'
		qry = Database.QueryDelete(tx, "table1")
		qry.execute()
		qry.finalize()

	if "s" in arg: 
		print 'SELECT'
		qry = Database.QuerySelect(tx, "table1")
		qry.execute()
		for row in qry:
			print '%s, %s' % (row[0].value, row[1].value)
		qry.finalize()

	tx.commit()
#	tx.rollback()
	db.closedb()

#	print 'INSERT'
#	qry = Database.QueryInsert(tx, "table1")
#	qry.execute()
#	qry.set_data(DatatypeInt64(2000), DatatypeString("cecil"))
#	qry.step()
#	qry.set_data(DatatypeInt64(3000), DatatypeString("borek"))
#	qry.step()
#	qry.finalize()
#
#	print 'SELECT'
#	qry = Database.QuerySelect(tx, "table1")
#	qry.execute()
#	for row in qry:
#		print row[0].value
#		print row[1].value
#	qry.finalize()
#
#	print 'UPDATE'
#	qry = Database.QueryUpdate(tx, "table1")
##	qry.set_data(DatatypeInt64(5000), None)
#	qry.set_callback(increase_pkey, None)
#	qry.execute()
#	qry.finalize()
#
#	print 'SELECT'
#	qry = Database.QuerySelect(tx, "table1")
#	qry.execute()
#	for row in qry:
#		print row[0].value
#		print row[1].value
#	qry.finalize()
#
#	tx.commit()
#	db.closedb()
#
#	print '#1 SELECT'
#	qry = Database.QuerySelect(tx, "table1")
#	for row in qry:
#		print row[0], row[1]
#	qry.finalize()
#
#	print '#2 DELETE'
#	qry = Database.QueryDelete(tx, "table1")
#	cond1 = Database.ConditionPredicate(0, "eq", 2000)
#	qry.set_conditions([ [ cond1 ] ])
#	qry.execute()
#	qry.finalize()
#
#	print '#3 SELECT'
#	qry = Database.QuerySelect(tx, "table1")
#	for row in qry:
#		print row[0], row[1]
#	qry.finalize()
#
#	print '#4 INSERT'
#	qry = Database.QueryInsert(tx, "table1")
#	qry.execute()
#	qry.set_data(2000, "cecil")
#	qry.step()
#	qry.finalize()
#	
#	print '#5 SELECT'
#	qry = Database.QuerySelect(tx, "table1")
#	for row in qry:
#		print row[0], row[1]
#	qry.finalize()
